线程安全之ThreadLocal

ThreadLocal为开发者提供了线程安全的另外一种思路:synchronized关注的是线程间数据的共享,而ThreadLocal关注的是线程间数据的隔离。

Why

当我们使用诸如synchronized、volatile等手段对线程进行同步时,多个线程会访问同样的变量,针对这种情况我们需要严格限制变量的访问,比如竞争、加锁、释放锁等操作,编码复杂度较高。
ThreadLocal提供了另外一种思路,顾名思义就是使用线程本地的变量,即每个ThreadLocal实例存储着只能被该线程访问和修改的变量,其他的线程无法访问。

How-To

ThreadLocal包含以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建, 无默认值
ThreadLocal<String> strThreadLocal = new ThreadLocal<>();
// 带默认值创建
ThreadLocal<Long> longThreadLocal = new longThreadLocal<Long>(){
@Override
protected Long initialValue() {
return new Long(101); // 即使remove后,get仍为此值
}
};
// 设值
strThreadLocal.set("luck");
// 取值
String v = strThreadLocal.get();
// 清空
strThreadLocal.remove();

使用起来非常简单,通常只需在线程的run方法内进行取值及设值即可。

How

ThreadLocal能保证线程中的变量不被其他线程干扰,让我们看看源码是如何做到的。
我们先来看Thread类,Thread类实例持有一个成员变量,该变量为ThreadLocal类的内部类ThreadLocalMap的实例:

1
2
3
4
package java.lang;
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}

而ThreadLocal类的内部类ThreadLocalMap里面又定义了一个内部类Entry。Entry类继承自WeakReference类,其泛型为ThreadLocal,这便是存储变量的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
package java.lang;
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}

让我们看看如何设值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** 返回线程持有的ThreadLocalMap实例(成员变量threadLocals) **/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/** 默认的初值为空 **/
protected T initialValue() {
return null;
}
/** 创建ThreadLocalMap, 设值为<当前ThreadLocal实例, 值>, 并让线程持有该ThreadLocalMap实例 **/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/** 设值 **/
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 获取线程所持ThreadLocalMap实例
if (map != null) // 如果map已创建
map.set(this, value); // 设值 <当前ThreadLocal实例, 变量>
else
createMap(t, value); // 创建map <当前ThreadLocal实例, 值> + 线程持有该map
}

而对应的取值代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/** 设默认值 **/
private T setInitialValue() {
T value = initialValue(); // 除非子类重写,否则默认的初值为空
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); // 重设 <ThreadLocal实例自身, 变量>
else
createMap(t, value); // 创建map,并存入<当前线程, 变量>
return value;
}
/** 取值 **/
public T get() {
Thread t = Thread.currentThread(); // 当前线程
ThreadLocalMap map = getMap(t); // 当前线程所持map
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // map不为空时,以当前ThreadLocal实例为key取Entry,并返回值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); // 如果map为空,返回默认的初值
}

最后是清空:

1
2
3
4
5
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread()); // 获取当前线程所持map
if (m != null)
m.remove(this); // 如果map不为空, 清理以当前ThreadLocal实例为key的Entry
}

总而言之,往ThreadLocal实例设值时:

  1. 从当前线程获取其持有的ThreadLocalMap实例,将ThreadLocalMap实例的key设为ThreadLocal实例自身,value为要存储的变量。
  2. 如果没有则创建ThreadLocalMap实例,同样地设值其key和value,并让当前线程持有该ThreadLocalMap实例。

从ThreadLocal实例取值时:

  1. 读取当前线程所持ThreadLocalMap实例成员变量,再从该ThreadLocalMap实例中以当前ThreadLocal实例为key找到对应的value。

子线程

当使用ThreadLocal来保存变量时,其保存在ThreadLocalMap中的变量只有持有该map的变量可以访问。
而我们可以用InheritableThreadLocal,其保存的变量不仅当前线程可以访问,还可以在子线程中被访问,从而实现了变量的传递。

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
Thread root = new Thread(()->{
threadLocal.set("luck");
Thread leaf = new Thread(()->{
System.out.println(threadLocal.get());
});
leaf.start();
});
root.start();
}

内存泄漏

当使用ThreadLocal保存变量时,线程持有ThreadLocalMap实例,因此当线程由线程池管理时,由于线程存在复用,因此ThreadLocalMap实例不会消失,ThreadLocalMap中Entry的ThreadLocal实例key和value会被一直持有,从而导致内存泄漏。
然而,这看似经得起推敲的结论并不正确,因为从Entry的源码可以看到,key并不是强引用的ThreadLocal实例,而是一个指向该实例的WeakReference。
当一个对象失去所有强引用,只有弱引用时,该对象会被GC标记为可回收;当ThreadLocal实例key被回收后,ThreadLocalMap中会存在key为null的Entry,同时get/set/remove方法均会清理key为null的Entry。
由这些措施,ThreadLocal基本避免了内存泄漏的隐患,但开发人员仍应在使用完ThreadLocal后,调用其remove方法进行手动清理。

Where

从ThreadLocal的原理来看,其使用场景可以用一句话总结:使用线程局部变量。
因此可以推断使用场景包含如下几种:

  1. 数据库连接。
  2. Session管理。
  3. 事务管理。